package com.cirrusblue.caching.roo.addon;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.springframework.roo.classpath.PhysicalTypeDetails;
import org.springframework.roo.classpath.PhysicalTypeIdentifier;
import org.springframework.roo.classpath.PhysicalTypeMetadata;
import org.springframework.roo.classpath.details.DefaultFieldMetadata;
import org.springframework.roo.classpath.details.FieldMetadata;
import org.springframework.roo.classpath.details.MutableClassOrInterfaceTypeDetails;
import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue;
import org.springframework.roo.classpath.details.annotations.AnnotationMetadata;
import org.springframework.roo.classpath.details.annotations.DefaultAnnotationMetadata;
import org.springframework.roo.metadata.MetadataService;
import org.springframework.roo.model.JavaPackage;
import org.springframework.roo.model.JavaSymbolName;
import org.springframework.roo.model.JavaType;
import org.springframework.roo.process.manager.FileManager;
import org.springframework.roo.process.manager.MutableFile;
import org.springframework.roo.project.Dependency;
import org.springframework.roo.project.Path;
import org.springframework.roo.project.PathResolver;
import org.springframework.roo.project.ProjectMetadata;
import org.springframework.roo.project.ProjectOperations;
import org.springframework.roo.project.ProjectType;
import org.springframework.roo.support.lifecycle.ScopeDevelopment;
import org.springframework.roo.support.util.Assert;
import org.springframework.roo.support.util.FileCopyUtils;
import org.springframework.roo.support.util.TemplateUtils;
import org.springframework.roo.support.util.XmlElementBuilder;
import org.springframework.roo.support.util.XmlUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;


/**
 * Implementation of commands that are available via the Roo shell.
 *
 * @author Steve Wilson
 * @since 1.0
 */
@ScopeDevelopment
public class CachingOperations {
	
	private static Logger logger = Logger.getLogger(CachingOperations.class.getName());
	
	private FileManager fileManager;
	private MetadataService metadataService;
	private ProjectOperations projectOperations;
	
	public CachingOperations(FileManager fileManager, MetadataService metadataService, ProjectOperations projectOperations) {
		Assert.notNull(fileManager, "File manager required");
		Assert.notNull(metadataService, "Metadata service required");
		Assert.notNull(projectOperations, "ProjectOperation service required");
		this.fileManager = fileManager;
		this.metadataService = metadataService;
		this.projectOperations = projectOperations;
	}
	
	public boolean isProjectAvailable() {
		return getPathResolver() != null;
	}
	
//	/**
//	 * @param propertyName to obtain (required)
//	 * @return a message that will ultimately be displayed on the shell
//	 */
//	public String getProperty(CacheProvider propertyName) {
//		Assert.notNull(propertyName, "Property name required");
//		String internalName = "user.name";
//		if (CacheProvider.HOME_DIRECTORY.equals(propertyName)) {
//			internalName = "user.home";
//		}
//		return propertyName.getPropertyName() + " : " + System.getProperty(internalName);
//	}
	
	/**
	 * @return true if the user's project has been created
	 */
	public boolean isCachingAvailable() {
		PathResolver pr = getPathResolver();
		if (pr == null) {
			return false;
		}
		File file = new File(pr.getIdentifier(Path.SRC_MAIN_RESOURCES, "/META-INF/persistence.xml"));
		return file.exists();
	}

	public void configureCaching(JavaType javaType) {
		Assert.notNull(javaType, "JavaType is required");
		
		
		PathResolver pr = getPathResolver();
		Assert.notNull(pr, "Path resolver not found");
		String path = pr.getIdentifier(Path.SRC_MAIN_RESOURCES, "/META-INF/persistence.xml");
		File file = new File(path);
		MutableFile mutableFile;
		Document persistence = null;
		
		if (file.exists()) {
			try {
			mutableFile = fileManager.updateFile(path);
			persistence = XmlUtils.getDocumentBuilder().parse(mutableFile.getInputStream());
			} catch (Exception exception) {
				new IllegalStateException("Could not parse persistence.xml", exception);
			}
		} else {
			//nothing to do so return
			return;
		}
		
		String ctxPath = pr.getIdentifier(Path.SPRING_CONFIG_ROOT, "applicationContext.xml");
		File ctxFile = new File(ctxPath);
		MutableFile ctxMutableFile = null;
		Document ctx = null;
		
		if (ctxFile.exists()) {
			try {
			ctxMutableFile = fileManager.updateFile(ctxPath);
			ctx = XmlUtils.getDocumentBuilder().parse(ctxMutableFile.getInputStream());
			} catch (Exception exception) {
				new IllegalStateException("Could not parse applicationContext.xml", exception);
			}
		} else {
			//nothing to do so return
			return;
		}
		
		Element provider = XmlUtils.findRequiredElement("/persistence/persistence-unit/provider", persistence.getDocumentElement());
		Assert.notNull(provider, "Could not find persistence provider setup in persistence.xml");
		
		Assert.notNull(ctxMutableFile, "Could not obtain applicationContext.xml");
		if (provider.getTextContent().startsWith("org.hibernate")) {
			Element beans = XmlUtils.findRequiredElement("/beans", ctx.getDocumentElement());
			beans.appendChild(new XmlElementBuilder("bean", ctx).addAttribute("class", "org.springframework.cache.ehcache.EhCacheFactoryBean").build());
		}
		
		XmlUtils.writeXml(ctxMutableFile.getOutputStream(), ctx);
		
		updateDependencies();
		
		addAnnotationToType(javaType);
		
		projectOperations.addDependency(new JavaPackage("com.sirrusblue.caching.roo.addon"), new JavaSymbolName("com.sirrusblue.caching.roo.addon"), "0.1.0-SNAPSHOT");
	}
	
	private void addAnnotationToType( JavaType javaType) {
	
		String declaredByMetadataId = PhysicalTypeIdentifier.createIdentifier(javaType, Path.SRC_MAIN_JAVA);
		
		// Obtain the physical type and itd mutable details
		PhysicalTypeMetadata ptm = (PhysicalTypeMetadata) metadataService.get(declaredByMetadataId);
		Assert.notNull(ptm, "Java source code unavailable for type " + javaType.getFullyQualifiedTypeName());
		PhysicalTypeDetails ptd = ptm.getPhysicalTypeDetails();
		Assert.notNull(ptd, "Java source code details unavailable for type " + javaType.getFullyQualifiedTypeName());
		Assert.isInstanceOf(MutableClassOrInterfaceTypeDetails.class, ptd, "Java source code is immutable for type " + javaType.getFullyQualifiedTypeName());
		MutableClassOrInterfaceTypeDetails mutableTypeDetails = (MutableClassOrInterfaceTypeDetails) ptd;

		if (!mutableTypeDetails.getTypeAnnotations().contains(new JavaType(RooCache.class.getName()))) {
			mutableTypeDetails.addTypeAnnotation(new DefaultAnnotationMetadata(new JavaType(RooCache.class.getName()), new ArrayList<AnnotationAttributeValue<?>>())); 
		}
	}
	
	private void updateDependencies() {	
		InputStream templateInputStream = TemplateUtils.getTemplate(getClass(), "dependencies.xml");
		Assert.notNull(templateInputStream, "Could not acquire dependencies.xml file");
		Document dependencyDoc;
		try {
			dependencyDoc = XmlUtils.getDocumentBuilder().parse(templateInputStream);
		} catch (Exception e) {
			throw new IllegalStateException(e);
		}

		Element dependenciesElement = (Element) dependencyDoc.getFirstChild();
		
		List<Element> cacheDependencies = XmlUtils.findElements("/dependencies/caching/hibernate/dependency", dependenciesElement);
		for(Element dependency : cacheDependencies) {
			projectOperations.dependencyUpdate(new Dependency(dependency));
		}
	}	
	
	/**
	 * @return the path resolver or null if there is no user project
	 */
	private PathResolver getPathResolver() {
		ProjectMetadata projectMetadata = (ProjectMetadata) metadataService.get(ProjectMetadata.getProjectIdentifier());
		if (projectMetadata == null) {
			return null;
		}
		return projectMetadata.getPathResolver();
	}

}